package dst.ass2.di.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

import dst.ass2.di.IInjectionController;
import dst.ass2.di.InjectionException;
import dst.ass2.di.annotation.Component;
import dst.ass2.di.annotation.ComponentId;
import dst.ass2.di.annotation.Inject;
import dst.ass2.di.model.ScopeType;

public class InjectionController implements IInjectionController{

	private AtomicLong IDGenerator = new AtomicLong(0L);  //use as ID Generator for Thread Safeness
	
	private HashMap<Class<?>, Object> singletonList = new HashMap<Class<?>, Object>();  //safe all singleton

	private final static Logger LOGGER = Logger.getLogger(InjectionController.class.getName());
	
	@Override
	public void initialize(Object obj) throws InjectionException {
		Class<?> cclass = obj.getClass();	//get the class from to intialized object

		LOGGER.info("call initialize on: "+cclass.getName());
	    
        boolean componentAnnotationFound = false;			//no component found
        boolean componentIDFound = false;
        final Long componentID = IDGenerator.getAndIncrement();	//get unique ID

        Map<String, Annotation> names = new HashMap<String, Annotation>();
        
        
        //ANNOTATION CHECK COMPONENT & SINGLETON
        Class<?> startClass=obj.getClass();
        while(startClass!=Object.class){
        	
        	//SINGLETON
        	if (isSingletonAnnotated(startClass)) {   //check if class is annotated with SINGLETON
                synchronized (singletonList) {
                    if (!singletonList.containsKey(startClass)) {			//if component is not in singleton list
                    	singletonList.put(startClass, obj);				   //add it
                    }
                    else{							//if it is already in the list (can only be one)
                    	throw new InjectionException("Singleton Component already known");		//throw Exception
                    }
                }
            }
        	
        	//COMPONENT
        	if (isComponentAnnotated(startClass)){			//if class is annotated with Component      		
        		componentAnnotationFound = true;
        		
        		if(initializeClassFields(obj, startClass, componentID, names)) //ID is setted
        			componentIDFound=true;
        	}
        	
        	//preparation for next round
        	startClass=startClass.getSuperclass();   //check superclass in next round
        	
        }

        if (!componentAnnotationFound) {  //check Annotation
            throw new InjectionException("No Component Annotation was found in hierarchy");
        }

        if (!componentIDFound) {		//check ID
            throw new InjectionException("No ID for Component was found");
        }
	}

	
	@SuppressWarnings("unchecked")
	@Override
	public <T> T getSingletonInstance(Class<T> cclass) throws InjectionException {
		if (!isSingletonAnnotated(cclass)) {
            throw new InjectionException("No Singleton annotated component found: "+cclass.getName());
        }

        synchronized(singletonList) {
            if (!singletonList.containsKey(cclass)) {		//if singleton is not in list
                try {
                    T instance = cclass.newInstance();		//initialize
                    initialize(instance);						//initialize
                    singletonList.put(cclass, instance);
                } catch (InstantiationException e) {
                    throw new InjectionException(e);
                } catch (IllegalAccessException e) {
                    throw new InjectionException(e);
                }
            }

            return (T)singletonList.get(cclass);				//retrieve from list
        }
	}
	
	
	//checks if the Class is annotated as Component
	private static boolean isComponentAnnotated(Class<?> cclass) {
        for (Annotation annotation : cclass.getDeclaredAnnotations()) {  //iterate over all Annotations in class
            if (annotation.annotationType() == Component.class) {			//compare annotationType
            	LOGGER.info("Class: "+cclass.getName()+ " is a Component");
                return true;
            }
        }

        return false;
    }

	
	//check if class is annotated as Singleton
    private static boolean isSingletonAnnotated(Class<?> cclass) {
        for (Annotation annotation : cclass.getDeclaredAnnotations()) {
            if (annotation.annotationType() == Component.class) {
                Component c = (Component)annotation;
                if (c.scope() == ScopeType.SINGLETON){
                	LOGGER.info("Class: "+cclass.getName()+ " is a Singleton");
                	return true;
                }
            }
        }

        return false;
    }

    /**
     * check if Field ID is of the right type and set it
     */
    private boolean checkAndSetId(Long id, Object obj, Field field) {
        boolean isSetted=false;
    	
    	if (field.getType() != Long.class) {
            throw new InjectionException("ID is required of type Long");
        }
    	else{
	        final boolean fieldAccessibility = field.isAccessible();   //is the field Accessible
	
	        try {
	            field.setAccessible(true);				//set it Accessible to set the ID
	            field.set(obj, id);						//set the ID
	            isSetted=true;
	            LOGGER.info("Field: "+field.getName()+ " has ID "+id);
	        } catch (IllegalArgumentException e) {
	            throw new InjectionException(e);
	        } catch (IllegalAccessException e) {
	            throw new InjectionException(e);
	        } finally {
	            field.setAccessible(fieldAccessibility);		//set it back
	        }
    	}
        
        return isSetted;
    }

    /**
     * check if annotation of Inject is required
     */
    private void checkRequiredAndInjectFieldClass (Object obj, Field field, Inject annotation) {
        Exception exc = doInjectionOfFieldClass(obj, field, annotation);	//inject the object

        if (exc != null && annotation.required()) {			//if it is required and an exception occurs a exception must be thrown
        	LOGGER.info("Field: "+field.getName()+ " raise an Error on injecting");
        	throw new InjectionException(exc);
        }
    }

    
    /**
     * perform injection with the object, field and annotation
     */
    private Exception doInjectionOfFieldClass(Object obj, Field field, Inject annotation) {
    	
    	LOGGER.info("Call doInjectionOfFieldClass");
    	
    	//assignment: and to specify a concrete subtype that should be instantiated (specificType)
    	//this specific type is optional (if not present, the declared type is used for injection)
    	
    	Class<?> subtype;
    	
    	if(annotation.specificType()==null || annotation.specificType() == Void.class){		//if not present
    		subtype=field.getType();		//declared type
    	}
    	else{
    		subtype=annotation.specificType();	//specific type
    	}

        if (!isComponentAnnotated(subtype)) {			//check if field object is annotated with Component annotation
            return new InjectionException("No Component found");
        }
        else{ 
	        final boolean fieldAccessibility = field.isAccessible();
	
	        //start the initialization
	        try {
	            Object inj = null;
	            if (isSingletonAnnotated(subtype)) {			//if singleton then get singleton from Map
	                inj = getSingletonInstance(subtype);
	            } else {
	                inj = subtype.newInstance();		//otherwise initialize new object
	                initialize(inj);				//recursion
	            }
	
	            field.setAccessible(true);			//make it writeable
	            field.set(obj, inj);				//set the injected object
	        } catch (Exception e) {
	            return e;
	        } finally {
	            field.setAccessible(fieldAccessibility);   //set the origin Accessibility
	        }
        }

        return null;
    }

    //check if Class should be initialized
    //check all fields in the class and inject (initialize) needed classes
    private boolean initializeClassFields(Object obj, final Class<?> cclass, final Long id, Map<String, Annotation> names) {
        boolean isIDExistingAndSetted = false;

        LOGGER.info("Call initializeClassFields on: "+cclass.getName());
        
        for (Field field : cclass.getDeclaredFields()) {   //retrieve all fields
            Annotation actInjectAnnotation = null;
            for (Annotation annotation : field.getDeclaredAnnotations()) {		//get annotation for each field
                Class<?> annotationType = annotation.annotationType();
                if (annotationType == ComponentId.class) {		//check if at least one field containts ComponendId annotation
                	isIDExistingAndSetted=checkAndSetId(id, obj, field);		//check type, accessibility and then set the id					//
                } 
                
                //assignment: every field of a component that is annotated by Inject has to be processed
                //it has to be possible to define wether the injection is required and to specify a concrete subtype that should be instantiated
                else if (annotationType == Inject.class) {	//check if at least one field contains Inject annotation
                    actInjectAnnotation = annotation;
                    checkRequiredAndInjectFieldClass(obj, field, (Inject)annotation); //check if required and inject
                }
            }

            if (isNamingAmbiguity(names, field.getName(), actInjectAnnotation)) {
            	LOGGER.info("Ambiguous name "+field.getName()+" on class "+cclass.getName());  //TODO exception
            	throw new InjectionException("Ambiguous name "+field.getName()+" on class "+cclass.getName());
            }
            
            names.put(field.getName(), actInjectAnnotation);  //add inject annotation to map
        }

        return isIDExistingAndSetted;
    }

    //assignment: report any naming ambiguities that occur
    private static boolean isNamingAmbiguity(Map<String, Annotation> names, String name, Annotation annotation) {
        boolean isAmbiguity=false;
    	
        LOGGER.info("Call isNamingAmbiguity");
        
        if (!names.containsKey(name)) {		//name not found
            return false;
        }

        final Inject otherAnnotation = (Inject)names.get(name);
        final Inject thisAnnotation = (Inject)annotation;

        if (otherAnnotation == null && thisAnnotation == null) {	//no Annotation
            return false;
        }

       if (otherAnnotation == null || thisAnnotation == null) {		//there is an Ambiguity
        	isAmbiguity=true;
        	LOGGER.info("AmbiguityCheck detects an Failure on: "+name+" with "+annotation.annotationType().getName());
       }

        if (otherAnnotation.required() != thisAnnotation.required() || otherAnnotation.specificType() != thisAnnotation.specificType()) {
        	isAmbiguity=true;
        	LOGGER.info("AmbiguityCheck detects an Failure on: "+name+" with "+annotation.annotationType().getName());
        }

        return isAmbiguity;
    }

}
